// Copyright (c)  YISH. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System;
using System.Text;
using NSQL.Expressions;
using NSQL.Expressions.Functions;
using NSQL.Extensions;

namespace NSQL.Generator
{
    public class SqlServerGenerator : BaseGenerator, IFunctionVisitor<StringBuilder>
    {
        public static SqlGeneratorSettings DefaultSettings => new SqlGeneratorSettings();
        public static readonly ISqlGenerator Instance = new SqlServerGenerator(DefaultSettings);
        private readonly SqlServerKeyWords _;


        
        StringBuilder IFunctionVisitor<StringBuilder>.Visit(INvlFunction nvlFunction)
        {
            var hasThen = !(nvlFunction.Then is null);

            if (hasThen)
            {
                return new MatchExpression(
                    new[]
                    {
                        new MatchPair<IPredicateExpression, IExpression>()
                        {
                            When = nvlFunction.Expr.IsNull(),
                            Then = nvlFunction.Default
                        }
                    },
                    nvlFunction.Then).Accept(this);
            }

            var builder = new StringBuilder();
            builder.Append(_.Coalesce);
            builder.Append('(');
            builder.Append(nvlFunction.Expr.Accept(this));
            builder.Append(", ").Append(nvlFunction.Default.Accept(this));
            builder.Append(')');
            return builder;
        }
        
        StringBuilder IFunctionVisitor<StringBuilder>.Visit(IExtractFunction extractFunction)
        {
            var builder = new StringBuilder();
            switch (extractFunction.Type)
            {
                case ExtractType.Year:
                    builder.Append(_.Year).Append('(');
                    break;
                case ExtractType.Month:
                    builder.Append(_.Month).Append('(');
                    break;
                case ExtractType.Day:
                    builder.Append(_.Day).Append('(');
                    break;
                case ExtractType.Hour:
                    builder.Append(_.DatePart).Append('(');
                    builder.Append(_.Hour);
                    builder.Append(", ");
                    break;
                case ExtractType.Minute:
                    builder.Append(_.DatePart).Append('(');
                    builder.Append(_.Minute);
                    builder.Append(", ");
                    break;
                case ExtractType.Second:
                    builder.Append(_.DatePart).Append('(');
                    builder.Append(_.Second);
                    builder.Append(", ");
                    break;
                case ExtractType.Week:
                    builder.Append(_.Week);
                    break;
                default:
                    throw new ArgumentOutOfRangeException();
            }
            builder.Append(extractFunction.DateTime.Accept(this));
            builder.Append(')');
            return builder;
        }

        StringBuilder IFunctionVisitor<StringBuilder>.Visit(ITrimFunction trimFunction)
        {
            if (trimFunction.TrimExpr is null)
            {
                var builder = new StringBuilder();
                
                switch (trimFunction.Type)
                {
                    case TrimType.Both:
                        builder.Append(_.Trim);
                        break;
                    case TrimType.Leading:
                        builder.Append(_.TrimStart);
                        break;
                    case TrimType.Trailing:
                        builder.Append(_.TrimEnd);
                        break;
                    default:
                        throw new ArgumentOutOfRangeException();
                }

                builder.Append('(').Append(trimFunction.Expr.Accept(this)).Append(')');
                return builder;
            }
            throw new NotSupportedException();
        }

        StringBuilder IFunctionVisitor<StringBuilder>.Visit(ILogFunction expression)
        {
            var builder = new StringBuilder();
            builder.Append(_.Log);

            var hasBase = !(expression.Base is null);

            if (hasBase)
            {
                if (expression.Base is ConstantExpression constantExpression && 
                    (constantExpression.ValueOf().Equals(10) || constantExpression.ValueOf().Equals(10d)))
                {
                    builder.Append("10");
                    hasBase = false;
                }
            }
            
            builder.Append('(');

            builder.Append(expression.Number.Accept(this));

            if (hasBase)
            {
                builder.Append(", ");
                builder.Append(expression.Base.Accept(this));
            }

            builder.Append(')');

            return builder;
        }


        StringBuilder IFunctionVisitor<StringBuilder>.Visit(IPercentileFunction expression)
        {
            var builder = new StringBuilder();
            builder.Append(_.Percentile);
            builder.Append('_');
            switch (expression.Type)
            {
                case PercentileType.Cont:
                    builder.Append(_.Cont);
                    break;
                case PercentileType.Disc:
                    builder.Append(_.Disc);
                    break;
                default:
                    throw new ArgumentOutOfRangeException();
            }

            builder.Append('(').Append(expression.P).Append(')');

            builder.Append(' ').Append(_.Within);
            builder.Append(' ').Append(_.Group);
            builder.Append(' ');
            builder.Append('(');
            builder.Append(_.Order);
            builder.Append(' ');
            builder.Append(_.By);
            builder.Append(' ').Append(expression.Expr.Accept(this));

            switch (expression.Order)
            {
                case Order.None:
                    break;
                case Order.Ascending:
                    builder.Append(' ').Append(_.Asc);
                    break;
                case Order.Descending:
                    builder.Append(' ').Append(_.Desc);
                    break;
                default:
                    throw new ArgumentOutOfRangeException();
            }

            builder.Append(')');

            builder.Append(' ')
                .Append(_.Over)
                .Append(' ')
                .Append('(').Append(_.Partition)
                .Append(' ').Append(_.By)
                .Append(' ').Append(0)
                .Append(')');

            return builder;
        }

        StringBuilder IFunctionVisitor<StringBuilder>.Visit(IAddIntervalFunction addIntervalFunction)
        {
            var builder = new StringBuilder();
            builder.Append(_.Date).Append(_.Add);
            builder.Append('(');
            switch (addIntervalFunction.Interval.Type)
            {
                case IntervalType.Years:
                    builder.Append(_.Year);
                    break;
                case IntervalType.Months:
                    builder.Append(_.Month);
                    break;
                case IntervalType.Days:
                    builder.Append(_.Day);
                    break;
                case IntervalType.Hours:
                    builder.Append(_.Hour);
                    break;
                case IntervalType.Minutes:
                    builder.Append(_.Minute);
                    break;
                case IntervalType.Seconds:
                    builder.Append(_.Second);
                    break;
                default:
                    throw new ArgumentOutOfRangeException();
            }


            builder.Append(", ").Append(addIntervalFunction.Interval.Interval);
            builder.Append(", ").Append(addIntervalFunction.DateTime.Accept(this));
            builder.Append(')');
            return builder;
        }

        

        StringBuilder IFunctionVisitor<StringBuilder>.Visit(IDiffFunction expression)
        {
            var build = new StringBuilder();
            build.Append(_.Date).Append(_.Diff).Append('(');
            switch (expression.Type)
            {
                case ExtractType.Year:
                    build.Append(_.Year);
                    break;
                case ExtractType.Month:
                    build.Append(_.Month);
                    break;
                case ExtractType.Week:
                    build.Append(_.Week);
                    break;
                case ExtractType.Day:
                    build.Append(_.Day);
                    break;
                case ExtractType.Hour:
                    build.Append(_.Hour);
                    break;
                case ExtractType.Minute:
                    build.Append(_.Minute);
                    break;
                case ExtractType.Second:
                    build.Append(_.Second);
                    break;
                default:
                    throw new ArgumentOutOfRangeException();
            }

            build.Append(", ").Append(expression.Start.Accept(this));
            build.Append(", ").Append(expression.End.Accept(this));
            build.Append(')');
            return build;
        }

        protected override StringBuilder FormatSelect(StringBuilder select, StringBuilder from, StringBuilder where,
            StringBuilder group, StringBuilder having, StringBuilder order, int skip, int limit)
        {
            if (skip <= 0 && limit <= 0)
            {
                return FormatSelect(select, from, where, group, having, order);
            }


            order = order?.Length > 0
                ? order
                : new StringBuilder().Append(1);

            var builder = FormatSelect(select, from, where, group, having, order);

            builder.Append(' ').Append(_.Offset).Append(' ');

            if (skip > 1)
            {
                builder.Append(skip).Append(' ').Append(_.Rows);
            }
            else
            {
                builder.Append(skip == 1 ? 1 : 0).Append(' ').Append(_.Row);
            }

            if (limit <= 0) return builder;

            builder
                .Append(' ').Append(_.Fetch)
                .Append(' ').Append(_.Next)
                .Append(' ').Append(limit)
                .Append(' ').Append(limit > 1 ? _.Rows : _.Row)
                .Append(' ').Append(_.Only);
            return builder;
        }
        
        StringBuilder IFunctionVisitor<StringBuilder>.Visit(ICastFunction castFunction)
        {
            var targetType = castFunction.TargetType;
            var builder = new StringBuilder();
            if (string.Equals(targetType, "text", StringComparison.OrdinalIgnoreCase))
            {
                builder.Append(_.Convert).Append('(')
                    .Append(_.Varchar).Append('(').Append(8000).Append(')')
                    .Append(", ").Append(castFunction.Expr.Accept(this))
                    .Append(')');

                return builder;
            }
            if (string.Equals(targetType, "numeric", StringComparison.OrdinalIgnoreCase))
            {
                builder.Append(_.Convert).Append('(')
                    .Append(castFunction.TargetType).Append('(').Append(38).Append(", ").Append(19).Append(')')
                    .Append(", ").Append(castFunction.Expr.Accept(this))
                    .Append(')');

                return builder;
            }

            if (string.Equals(targetType, "int", StringComparison.OrdinalIgnoreCase)||
                string.Equals(targetType, "float", StringComparison.OrdinalIgnoreCase))
            {
                builder.Append(_.Cast)
                    .Append('(').Append(castFunction.Expr.Accept(this)).Append(' ')
                    .Append(_.As).Append(' ').Append(targetType).Append(')');

                return builder;
            }

            if (string.Equals(targetType, "timestamp", StringComparison.OrdinalIgnoreCase))
            {
                targetType = "datetime";
            }
            builder.Append(_.Convert);
            builder.Append('(');
            builder.Append(targetType);
            builder.Append(", ");
            builder.Append(castFunction.Expr.Accept(this));
            builder.Append(')');
            return builder;
        }
        
        public SqlServerGenerator(Func<SqlGeneratorSettings, SqlGeneratorSettings> setupAction)
            : this(setupAction(DefaultSettings))
        {
        }
        
        public SqlServerGenerator(SqlGeneratorSettings settings) : this(settings, new SqlServerKeyWords(settings.LetterCase))
        {
        }
        private SqlServerGenerator(SqlGeneratorSettings settings, SqlServerKeyWords keyWords) : base(settings, keyWords)
        {
            _ = keyWords;
        }
        
        protected class SqlServerKeyWords : KeyWords 
        {
            public SqlServerKeyWords(LetterCase letterCase) : base(letterCase)
            {
            }
            public string Partition => FormatLetter(nameof(Partition));
            public override string Now => FormatLetter("GetDate()");

            public override string Length => FormatLetter("Len");
            public string Offset => FormatLetter(nameof(Offset));
            public string Convert => FormatLetter(nameof(Convert));
            public string Row => FormatLetter(nameof(Row));
            public string Rows => FormatLetter(nameof(Rows));
            public string Fetch => FormatLetter(nameof(Fetch));
            public string Next => FormatLetter(nameof(Next));
            public string Only => FormatLetter(nameof(Only));
            public string DatePart => FormatLetter(nameof(DatePart));
            public override string Ceil => FormatLetter("Ceiling");
            public override string TrimStart => FormatLetter("LTrim");
            public override string TrimEnd => FormatLetter("RTrim");
        
            public override string Variance => FormatLetter("Var");
        
            public override string StdDev => FormatLetter("Stdev");
        }
    }
}